/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.code;

import java.util.ArrayList;

import com.android.dx.rop.code.RegisterSpec;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.type.TypeBearer;
import com.android.dx.util.ExceptionWithContext;
import com.android.dx.util.Hex;

/**
 * Representation of a set of local variable arrays, with Java semantics. This
 * peculiar case is to support in-method subroutines, which can have different
 * locals sets for each caller.
 * <p>
 * <b>Note:</b> For the most part, the documentation for this class ignores the
 * distinction between {@link com.android.dx.rop.type.Type} and
 * {@link com.android.dx.rop.type.TypeBearer}.
 * </p>
 */
public class LocalsArraySet extends LocalsArray {

	/**
	 * The primary LocalsArray represents the locals as seen from the subroutine
	 * itself, which is the merged representation of all the individual locals
	 * states.
	 */
	private final OneLocalsArray primary;

	/**
	 * Indexed by label of caller block: the locals specific to each caller's
	 * invocation of the subroutine.
	 */
	private final ArrayList<LocalsArray> secondaries;

	/**
	 * Constructs an instance. The locals array initially consists of
	 * all-uninitialized values (represented as {@code null}s).
	 * 
	 * @param maxLocals
	 *            {@code >= 0;} the maximum number of locals this instance can
	 *            refer to
	 */
	public LocalsArraySet(int maxLocals) {
		super(maxLocals != 0);
		primary = new OneLocalsArray(maxLocals);
		secondaries = new ArrayList();
	}

	/**
	 * Constructs an instance with the specified primary and secondaries set.
	 * 
	 * @param primary
	 *            {@code non-null;} primary locals to use
	 * @param secondaries
	 *            {@code non-null;} secondaries set, indexed by subroutine
	 *            caller label.
	 */
	public LocalsArraySet(OneLocalsArray primary,
			ArrayList<LocalsArray> secondaries) {
		super(primary.getMaxLocals() > 0);

		this.primary = primary;
		this.secondaries = secondaries;
	}

	/**
	 * Constructs an instance which is a copy of another.
	 * 
	 * @param toCopy
	 *            {@code non-null;} instance to copy.
	 */
	private LocalsArraySet(LocalsArraySet toCopy) {
		super(toCopy.getMaxLocals() > 0);

		primary = toCopy.primary.copy();
		secondaries = new ArrayList(toCopy.secondaries.size());

		int sz = toCopy.secondaries.size();
		for (int i = 0; i < sz; i++) {
			LocalsArray la = toCopy.secondaries.get(i);

			if (la == null) {
				secondaries.add(null);
			} else {
				secondaries.add(la.copy());
			}
		}
	}

	/** @inheritDoc */
	@Override
	public void setImmutable() {
		primary.setImmutable();

		for (LocalsArray la : secondaries) {
			if (la != null) {
				la.setImmutable();
			}
		}
		super.setImmutable();
	}

	/** @inheritDoc */
	@Override
	public LocalsArray copy() {
		return new LocalsArraySet(this);
	}

	/** @inheritDoc */
	@Override
	public void annotate(ExceptionWithContext ex) {
		ex.addContext("(locals array set; primary)");
		primary.annotate(ex);

		int sz = secondaries.size();
		for (int label = 0; label < sz; label++) {
			LocalsArray la = secondaries.get(label);

			if (la != null) {
				ex.addContext("(locals array set: primary for caller "
						+ Hex.u2(label) + ')');

				la.getPrimary().annotate(ex);
			}
		}
	}

	/**  */
	public String toHuman() {
		StringBuilder sb = new StringBuilder();

		sb.append("(locals array set; primary)\n");

		sb.append(getPrimary().toHuman());
		sb.append('\n');

		int sz = secondaries.size();
		for (int label = 0; label < sz; label++) {
			LocalsArray la = secondaries.get(label);

			if (la != null) {
				sb.append("(locals array set: primary for caller "
						+ Hex.u2(label) + ")\n");

				sb.append(la.getPrimary().toHuman());
				sb.append('\n');
			}
		}

		return sb.toString();
	}

	/** @inheritDoc */
	@Override
	public void makeInitialized(Type type) {
		int len = primary.getMaxLocals();

		if (len == 0) {
			// We have to check for this before checking for immutability.
			return;
		}

		throwIfImmutable();

		primary.makeInitialized(type);

		for (LocalsArray la : secondaries) {
			if (la != null) {
				la.makeInitialized(type);
			}
		}
	}

	/** @inheritDoc */
	@Override
	public int getMaxLocals() {
		return primary.getMaxLocals();
	}

	/** @inheritDoc */
	@Override
	public void set(int idx, TypeBearer type) {
		throwIfImmutable();

		primary.set(idx, type);

		for (LocalsArray la : secondaries) {
			if (la != null) {
				la.set(idx, type);
			}
		}
	}

	/** @inheritDoc */
	@Override
	public void set(RegisterSpec spec) {
		set(spec.getReg(), spec);
	}

	/** @inheritDoc */
	@Override
	public void invalidate(int idx) {
		throwIfImmutable();

		primary.invalidate(idx);

		for (LocalsArray la : secondaries) {
			if (la != null) {
				la.invalidate(idx);
			}
		}
	}

	/** @inheritDoc */
	@Override
	public TypeBearer getOrNull(int idx) {
		return primary.getOrNull(idx);
	}

	/** @inheritDoc */
	@Override
	public TypeBearer get(int idx) {
		return primary.get(idx);
	}

	/** @inheritDoc */
	@Override
	public TypeBearer getCategory1(int idx) {
		return primary.getCategory1(idx);
	}

	/** @inheritDoc */
	@Override
	public TypeBearer getCategory2(int idx) {
		return primary.getCategory2(idx);
	}

	/**
	 * Merges this set with another {@code LocalsArraySet} instance.
	 * 
	 * @param other
	 *            {@code non-null;} to merge
	 * @return {@code non-null;} this instance if merge was a no-op, or new
	 *         merged instance.
	 */
	private LocalsArraySet mergeWithSet(LocalsArraySet other) {
		OneLocalsArray newPrimary;
		ArrayList<LocalsArray> newSecondaries;
		boolean secondariesChanged = false;

		newPrimary = primary.merge(other.getPrimary());

		int sz1 = secondaries.size();
		int sz2 = other.secondaries.size();
		int sz = Math.max(sz1, sz2);
		newSecondaries = new ArrayList(sz);

		for (int i = 0; i < sz; i++) {
			LocalsArray la1 = (i < sz1 ? secondaries.get(i) : null);
			LocalsArray la2 = (i < sz2 ? other.secondaries.get(i) : null);
			LocalsArray resultla = null;

			if (la1 == la2) {
				resultla = la1;
			} else if (la1 == null) {
				resultla = la2;
			} else if (la2 == null) {
				resultla = la1;
			} else {
				try {
					resultla = la1.merge(la2);
				} catch (SimException ex) {
					ex.addContext("Merging locals set for caller block "
							+ Hex.u2(i));
				}
			}

			secondariesChanged = secondariesChanged || (la1 != resultla);

			newSecondaries.add(resultla);
		}

		if ((primary == newPrimary) && !secondariesChanged) {
			return this;
		}

		return new LocalsArraySet(newPrimary, newSecondaries);
	}

	/**
	 * Merges this set with a {@code OneLocalsArray} instance.
	 * 
	 * @param other
	 *            {@code non-null;} to merge
	 * @return {@code non-null;} this instance if merge was a no-op, or new
	 *         merged instance.
	 */
	private LocalsArraySet mergeWithOne(OneLocalsArray other) {
		OneLocalsArray newPrimary;
		ArrayList<LocalsArray> newSecondaries;
		boolean secondariesChanged = false;

		newPrimary = primary.merge(other.getPrimary());
		newSecondaries = new ArrayList(secondaries.size());

		int sz = secondaries.size();
		for (int i = 0; i < sz; i++) {
			LocalsArray la = secondaries.get(i);
			LocalsArray resultla = null;

			if (la != null) {
				try {
					resultla = la.merge(other);
				} catch (SimException ex) {
					ex.addContext("Merging one locals against caller block "
							+ Hex.u2(i));
				}
			}

			secondariesChanged = secondariesChanged || (la != resultla);

			newSecondaries.add(resultla);
		}

		if ((primary == newPrimary) && !secondariesChanged) {
			return this;
		}

		return new LocalsArraySet(newPrimary, newSecondaries);
	}

	/** @inheritDoc */
	@Override
	public LocalsArraySet merge(LocalsArray other) {
		LocalsArraySet result;

		try {
			if (other instanceof LocalsArraySet) {
				result = mergeWithSet((LocalsArraySet) other);
			} else {
				result = mergeWithOne((OneLocalsArray) other);
			}
		} catch (SimException ex) {
			ex.addContext("underlay locals:");
			annotate(ex);
			ex.addContext("overlay locals:");
			other.annotate(ex);
			throw ex;
		}

		result.setImmutable();
		return result;
	}

	/**
	 * Gets the {@code LocalsArray} instance for a specified subroutine caller
	 * label, or null if label has no locals associated with it.
	 * 
	 * @param label
	 *            {@code >= 0;} subroutine caller label
	 * @return {@code null-ok;} locals if available.
	 */
	private LocalsArray getSecondaryForLabel(int label) {
		if (label >= secondaries.size()) {
			return null;
		}

		return secondaries.get(label);
	}

	/** {@inheritDoc} */
	@Override
	public LocalsArraySet mergeWithSubroutineCaller(LocalsArray other,
			int predLabel) {

		LocalsArray mine = getSecondaryForLabel(predLabel);
		LocalsArray newSecondary;
		OneLocalsArray newPrimary;

		newPrimary = primary.merge(other.getPrimary());

		if (mine == other) {
			newSecondary = mine;
		} else if (mine == null) {
			newSecondary = other;
		} else {
			newSecondary = mine.merge(other);
		}

		if ((newSecondary == mine) && (newPrimary == primary)) {
			return this;
		} else {
			/*
			 * We're going to re-build a primary as a merge of all the
			 * secondaries.
			 */
			newPrimary = null;

			int szSecondaries = secondaries.size();
			int sz = Math.max(predLabel + 1, szSecondaries);
			ArrayList<LocalsArray> newSecondaries = new ArrayList(sz);
			for (int i = 0; i < sz; i++) {
				LocalsArray la = null;

				if (i == predLabel) {
					/*
					 * This LocalsArray always replaces any existing one, since
					 * this is the result of a refined iteration.
					 */
					la = newSecondary;
				} else if (i < szSecondaries) {
					la = secondaries.get(i);
				}

				if (la != null) {
					if (newPrimary == null) {
						newPrimary = la.getPrimary();
					} else {
						newPrimary = newPrimary.merge(la.getPrimary());
					}
				}

				newSecondaries.add(la);
			}

			LocalsArraySet result = new LocalsArraySet(newPrimary,
					newSecondaries);
			result.setImmutable();
			return result;
		}
	}

	/**
	 * Returns a LocalsArray instance representing the locals state that should
	 * be used when returning to a subroutine caller.
	 * 
	 * @param subLabel
	 *            {@code >= 0;} A calling label of a subroutine
	 * @return {@code null-ok;} an instance for this subroutine, or null if
	 *         subroutine is not in this set.
	 */
	public LocalsArray subArrayForLabel(int subLabel) {
		LocalsArray result = getSecondaryForLabel(subLabel);
		return result;
	}

	/** {@inheritDoc} */
	@Override
	protected OneLocalsArray getPrimary() {
		return primary;
	}
}
